import 'package:flutter_oss_aliyun/src/extension/date_extension.dart';
import 'package:flutter_oss_aliyun/src/model/request.dart';
import 'package:flutter_oss_aliyun/src/model/signed_parameters.dart';
import 'package:flutter_oss_aliyun/src/util/encrypt.dart';

// 完全参考[flutter_oss_aliyun](https://pub-web.flutter-io.cn/packages/flutter_oss_aliyun#download-object) Auth类代码
// 但是flutter_oss_aliyun只支持STS鉴权模式，因此在此基础上进行修改

/// STS鉴权模式
class OSSAuthSTS extends OSSAuth {
  /// sts鉴权
  ///
  /// [tempAK]、[tempAS]为临时的accessKey和accessSecret
  ///
  /// [expire]表示[tempAK]和[tempAS]过期时间，时间戳形式。此值为具体的时刻(如2025-01-04 12:00对应的时间戳)，不是指有效时长。
  const OSSAuthSTS({
    required String tempAK,
    required String tempAS,
    required String secureToken,
    required int expire,
  }) : super._inner(
            accessKey: tempAK,
            accessSecret: tempAS,
            secureToken: secureToken,
            expire: expire);
}

/// 明文授权模式
class OSSAuth {
  const OSSAuth._inner(
      {required this.accessKey,
      required this.accessSecret,
      this.secureToken,
      this.expire});

  const OSSAuth({
    required String accessKey,
    required String accessSecret,
  }) : this._inner(accessKey: accessKey, accessSecret: accessSecret);

  final String accessKey;
  final String accessSecret;
  final String? secureToken;
  final int? expire;

  bool get isExpired =>
      expire == null ? false : DateTime.now().millisecondsSinceEpoch > expire!;

  String? get encodedToken => secureToken?.replaceAll("+", "%2B");

  /// access aliyun need authenticated, this is the implementation refer to the official document.
  /// [req] include the request headers information that use for auth.
  /// [bucket] is the name of bucket used in aliyun oss
  /// [key] is the object name in aliyun oss, alias the 'filepath/filename'
  void sign(HttpRequest req, String bucket, String key) {
    req.headers['x-oss-date'] = DateTime.now().toGMTString();
    if (secureToken != null) {
      req.headers['x-oss-security-token'] = secureToken;
    }
    final String signature = _makeSignature(req, bucket, key);
    req.headers['Authorization'] = "OSS $accessKey:$signature";
  }

  /// the signature of file
  /// [expires] expired time (seconds)
  /// [bucket] is the name of bucket used in aliyun oss
  /// [key] is the object name in aliyun oss, alias the 'filepath/filename'
  String getSignature(
    int expires,
    String bucket,
    String key, {
    Map<String, dynamic>? params,
  }) {
    final String queryString = params == null
        ? ""
        : params.entries
            .where((entry) => entry.key.toLowerCase().startsWith('x-oss-'))
            .map((entry) => "${entry.key}=${entry.value}")
            .join("&");
    final String paramString = queryString.isEmpty ? "" : "&$queryString";

    final String stringToSign = [
      "GET",
      "",
      '',
      expires,
      "${_getResourceString(bucket, key, {})}?security-token=$secureToken$paramString"
    ].join("\n");
    final String signed = EncryptUtil.hmacSign(accessSecret, stringToSign);

    return Uri.encodeFull(signed).replaceAll("+", "%2B");
  }

  /// sign the string use hmac
  String _makeSignature(HttpRequest req, String bucket, String key) {
    final String contentMd5 = req.headers['content-md5'] ?? '';
    final String contentType = req.headers['content-type'] ?? '';
    final String date = req.headers['x-oss-date'] ?? '';
    final String headerString = _getHeaderString(req);
    final String resourceString = _getResourceString(bucket, key, req.param);
    final String stringToSign = [
      req.method,
      contentMd5,
      contentType,
      date,
      headerString,
      resourceString
    ].join("\n");

    return EncryptUtil.hmacSign(accessSecret, stringToSign);
  }

  /// sign the header information
  String _getHeaderString(HttpRequest req) {
    final List<String> ossHeaders = req.headers.keys
        .where((key) => key.toLowerCase().startsWith('x-oss-'))
        .toList();
    if (ossHeaders.isEmpty) return '';
    ossHeaders.sort((s1, s2) => s1.compareTo(s2));

    return ossHeaders.map((key) => "$key:${req.headers[key]}").join("\n");
  }

  /// sign the resource part information
  String _getResourceString(
    String bucket,
    String fileKey,
    Map<String, dynamic> param,
  ) {
    String path = "/";
    if (bucket.isNotEmpty) path += "$bucket/";
    if (fileKey.isNotEmpty) path += fileKey;
    final String signedParamString = param.keys
        .where((key) => SignParameters.signedParams.contains(key))
        .map((item) => "$item=${param[item]}")
        .join("&");
    if (signedParamString.isNotEmpty) {
      path += "?$signedParamString";
    }

    return path;
  }
}
